
import os
import codecs
import tempfile

import ab_ids as ai
import ab_utils as utils


class _File:
    """it provides an encapsulation of interfaces for a single file"""
    def __init__(self, filename, mode, utf8=False, temp=False, **kws):
        self.filename = filename
        self.temp = temp

        self.file_ = utils.openfile(self.filename, mode, utf8=utf8)

        self.refresh = True
        self.size = len(self)

    def __repr__(self):
        return '<<File> name: %s, size: %s, temp: %s>' % (
            self.filename, self.size, self.temp)

    def __len__(self):
        if self.refresh:
            self.refresh = False
            self.file_.seek(0, os.SEEK_END)
            self.size = self.file_.tell()

        return int(self.size)

    def getName(self):
        return self.filename

    def isTemporary(self):
        return self.temp

    def read(self, size, offset):
        self.file_.seek(offset, os.SEEK_SET)
        return self.file_.read(size)

    def write(self, data):
        self.file_.write(data)
        self.refresh = True

    def __del__(self):
        try:
            self.file_.close()
            if self.temp:
                os.remove(self.filename)
        except IOError:
            pass


class File:
    def __init__(self, app, filename=None, mode='r', afile=None,
                 afiles=None, utf8=False, fields=None, **kws):

        self.fields = fields

        self.refresh = True
        self.iosize = app.readKey(ai.CfgParaBlockCacheSize)

        def __writefile(filename, utf8, data):
            with utils.openfile(filename, 'ab+', utf8=utf8) as file_:
                file_.write(data)

        if 'data' in kws or ('zone' in kws and isinstance(afile, File)):
            # write the data to a temporary file
            temp = filename or tempfile.mktemp('.aof')

            if 'data' in kws:
                __writefile(temp, utf8, kws.get('data', ''))
            else:
                # the usage of 'zone' should be replaced by 'fields' not
                # to write the temporary file.
                zone = kws.get('zone')
                data = afile.read(zone[1], zone[0])
                __writefile(temp, utf8, data)

            self.files = self.__clone(_File(temp, mode, utf8=utf8, temp=True))
        elif isinstance(afile, File):
            self.files = self.__clone(afile)
            if afile.fields and fields:
                raise Exception("both afile.fields and files are defined")

            if afile.fields:
                self.fields = afile.fields

            if afiles is not None:
                raise Exception(
                    "both afile and afiles can't be non-null together")
        elif afiles is not None:
            self.files = self.__clone(afiles)
        elif filename:
            self.files = self.__clone(_File(filename, mode, utf8=utf8))
        elif mode and mode.find('w') > -1:
            self.files = self.__clone(
                _File(tempfile.mktemp('.aof'), mode, temp=True))
        else:
            self.files = list()
            raise Exception('blabla')

        if mode and mode.find('w') > -1:
            if len(self.files) != 1:
                raise Exception("Mode write doesn't support multiple files")
            elif self.fields:
                raise Exception("Fields cannot be supported in write mode")

        self.offset = 0
        self.size = len(self)

        self.__verify()

    def __clone(self, file_):
        ret = list()

        if isinstance(file_, _File):
            ret.append(file_)
        elif isinstance(file_, File):
            ret.extend(file_.files)
        elif isinstance(file_, (list, tuple)):
            for ff in file_:
                if isinstance(ff, File):
                    ret.extend(ff.files)
                else:
                    raise Exception('unknown instance of ' + str(ff))

        return ret

    def __verify(self):
        if self.fields:
            sum, lstart, nstart = 0, 0, 0
            for fd in self.fields:
                start, size = fd.getZone()
                if start < 0 or start > self.size or size <= 0:
                    raise Exception('invalid field "%s"' % str(ff))
                elif start <= lstart:
                    raise Exception('start in fields should be ascent')
                elif nstart > start:
                    raise Exception('fields are overlaying')

                lstart = start
                nstart = start + size
                sum += size

            if sum > self.size:
                raise Exception('invalid field size sum')

            self.size = sum
            self.refresh = False

    def __repr__(self):
        ret = '<File>'
        for ff in self.files:
            ret += ' %s' % str(ff)

        return ret

    def __len__(self):
        if self.refresh:
            self.refresh = False

            self.size = 0
            for ff in self.files:
                self.size += len(ff)

        return int(self.size)

    def seek(self, offset, whence=0):
        size = len(self)

        if whence == os.SEEK_SET:
            offset = offset
        elif whence == os.SEEK_CUR:
            offset += offset
        elif whence == os.SEEK_END:
            offset = size - offset

        self.offset = min(size, max(0, offset))

    def read(self, length=-1, offset=-1):
        ret = ''

        if 0 <= offset <= len(self):
            self.seek(offset, os.SEEK_SET)

        if length == -1:
            length = len(self) - offset

        offset = 0
        if length > 0 and self.fields:
            for fd in self.fields:
                start, size = fd.getZone()
                if offset <= self.offset:
                    # find the start point
                    sum = 0
                    for ff in self.files:
                        if sum <= start + self.offset - offset < len(ff) and \
                                self.offset - offset < size:
                            lread = min(size - (self.offset - offset), length)
                            ret += ff.read(lread, start + self.offset - offset)

                            length -= lread
                            self.offset += lread
                            break

                        sum += len(ff)

                offset += size

        elif length > 0:
            for ff in self.files:
                if offset <= self.offset < offset + len(ff) and length > 0:
                    size = min(length, len(ff) - (self.offset - offset))
                    ret += ff.read(size, self.offset - offset)

                    length -= size
                    self.offset += size

                offset += len(ff)

        return ret

    def readline(self):
        ret = ''

        while True:
            c = self.read(1)
            ret += c
            if c == '\n' or c == '':
                break

        return ret

    def readlines(self):
        rets = list()

        while True:
            li = self.readline()
            if li == '':
                break

            rets.append(li)

        return rets

    def write(self, data, length=-1):
        if len(self.files) != 1:
            raise Exception('Wrong multiple files: %d' % len(self.files))
        elif self.fields:
            raise Exception('Write cannot be supported with fields')

        file_ = self.files[0]
        try:
            file_.write(data)
            self.size += length
            self.refresh = True

            return True
        except IOError:
            return False

    def getFilename(self):
        if len(self.files) == 1:
            return self.files[0].getName()
        else:
            return ''

    def find(self, data, offset, backward=False):
        ret = -1

        if backward:
            offset += len(data) - 1

        while ret == -1:
            fx = self.iosize
            if backward:
                fx = min(offset, fx)
                offset -= fx
                if offset > len(data):
                    offset -= len(data)

            content = self.read(fx, offset)
            try:
                if backward:
                    ret = data.rfind(content)
                else:
                    ret = data.find(content)
            except:
                if backward:
                    o = len(data) - 1
                    for k in xrange(len(content) - 1, 0, -1):
                        if ord(data[o]) != ord(content[k]):
                            o = len(data) - 1
                        else:
                            o -= 1
                            if o == -1:
                                ret = k - o - 1
                                break
                else:
                    o = 0
                    for (k, c) in enumerate(content):
                        if ord(data[o]) != ord(c):
                            o = 0
                        else:
                            o += 1
                            if o == len(data):
                                ret = k - o + 1
                                break

            if ret != -1:
                ret += offset

            if len(content) <= len(data):
                break

            if not backward:
                offset += len(content) - len(data)

        return ret

    def save(self, filename, offset=0, length=0, line=0,
             align=False, asc=False, notify=None):

        ret = False

        with utils.openfile(filename, 'wb') as f:
            ret = self.dump(offset, length, line, align,
                            asc, notify, f.write, nodump=True)

        return ret

    def dump(self, offset=0, length=0, line=0,
             align=False, asc=False, notify=None, writecb=None, nodump=False):

        def emtpyCallback(*args):
            pass

        if writecb is None:
            return False
        if notify is None:
            notify = emtpyCallback

        if length == 0:
            length = len(self)
        else:
            length = min(length + offset, len(self))

        if line == 0:
            while offset < length:
                lx = min(self.iosize, length - offset)
                writecb(self.read(lx, offset))
                offset += lx
                notify(int(offset * 100 / length))
        elif align or asc:
            oa = []
            ob = []
            st = int(offset / line) * line
            if asc:
                fmt = '%%08x | %%-%ds| %%-%ds%s' % (3 * line, line, os.linesep)
            else:
                fmt = '%%08x | %%-%ds%s' % (3 * line, os.linesep)

            if st < offset and align:
                for c in range(st, offset):
                    oa.append('  ')
                    ob.append(' ')
            else:
                st = offset

            while offset < length:
                li = ''
                lx = min(self.iosize, length - offset)
                da = self.read(lx, offset)
                for c in da:
                    oa.append('%02x' % ord(c))
                    if asc:
                        ob.append(['.', c][0x20 <= ord(c) < 0x7f])

                k = 0
                offset += len(da)
                while len(oa) - k >= line:
                    if asc:
                        li += fmt % (st, ' '.join(oa[k:k + line]),
                                     ''.join(ob[k:k + line]))
                    else:
                        li += fmt % (st, ' '.join(oa[k:k + line]))
                    k += line
                    st += line
                    notify(int(st * 100 / length))

                writecb(li)
                oa = oa[k:]
                if asc:
                    ob = ob[k:]

            if len(oa) > 0:
                if asc:
                    li = fmt % (st, ' '.join(oa), ''.join(ob))
                else:
                    li += fmt % (st, ' '.join(oa[k:k + line]))
                writecb(li)
        else:
            mu = int(self.iosize / line) * line
            while offset < length:
                li = ''
                lx = min(mu, length - offset)
                da = self.read(lx, offset)
                # do the assumation that output is hex format
                db = []
                for c in da:
                    db.append('%02x' % ord(c))

                for k in range(0, lx, line):
                    li += ' '.join(db[k:k + line])
                    if nodump:
                        li += os.linesep
                    else:
                        li += ' '
                    notify(int((length - offset) * 100 / length))

                writecb(li.strip())
                offset += lx

        notify(100)
        return True
